/*
 * This file is part of the Try! Engine project.
 *
 * Copyright 2010-2011 Emanuele Bertoldi. All rights reserved.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * You should have received a copy of the modified BSD License along with this
 * program. If not, see <http://www.opensource.org/licenses/bsd-license.php>
 */

#include "graphicsmanager.h"
#include "cameranode.h"
#include <GL/gl.h>

using namespace Try;

GraphicsManager::GraphicsManager(RenderContext* rc)
{
    this->registerRenderContext(rc);

    h3dInit();

    h3dSetOption(H3DOptions::LoadTextures, 1);
    h3dSetOption(H3DOptions::TexCompression, 0);
    h3dSetOption(H3DOptions::FastAnimation, 0);
    h3dSetOption(H3DOptions::MaxAnisotropy, 4);
    h3dSetOption(H3DOptions::ShadowMapSize, 2048);

    new SceneNode(H3DRootNode);
}

GraphicsManager::~GraphicsManager()
{
    int size = m_renderContexts.size();
    for (int i=size; i>0; i--)
        delete m_renderContexts.at(i-1);

    h3dRelease();
}

void GraphicsManager::registerRenderContext(RenderContext* rc)
{
    if (rc && !this->hasRenderContext(rc))
        m_renderContexts.push_back(rc);
}

void GraphicsManager::unregisterRenderContext(RenderContext* rc)
{
    if (rc)
    {
        for (int i=0; i<m_renderContexts.size(); i++)
        {
            if (m_renderContexts.at(i) == rc)
            {
                m_renderContexts.erase(m_renderContexts.begin() + i);
                break;
            }
        }
    }
}

void GraphicsManager::render(RenderContext* rc, double dt)
{
    if (rc)
    {
        CameraNode* cam = rc->activeCamera();

        if (cam)
        {
            std::vector<RenderListener*> listeners = this->listeners();
            int size = listeners.size();

            for (int i=0; i<size; ++i)
                listeners.at(i)->onRenderStarted(rc, dt);

            h3dRender(cam->nodeHandle());

            this->renderPrimitives(rc);

            for (int i=0; i<size; ++i)
                listeners.at(i)->onRenderEnded(rc);
        }

        rc->updateContext();
    }
}

void GraphicsManager::renderAll(double dt)
{

    for (int i=0; i<this->listenerCount(); ++i)
        this->listeners().at(i)->onRenderStarted(dt);

    for (int i=0; i<this->renderContextCount(); i++)
        this->render(m_renderContexts.at(i), dt);

    this->finalizeRender();

    for (int i=0; i<this->listenerCount(); ++i)
        this->listeners().at(i)->onRenderEnded();
}

int GraphicsManager::renderContextCount() const
{
    return m_renderContexts.size();
}

bool GraphicsManager::hasRenderContext(RenderContext* rc) const
{
    if (rc)
    {
        for (int i=0; i<m_renderContexts.size(); i++)
            if (m_renderContexts.at(i) == rc)
                return true;
    }

    return false;
}

void GraphicsManager::drawLine(const Vector3& start, const Vector3& end, const Color& color)
{
    int index = m_lineIndex;

    this->addLineVertex(start, color);
    this->addLineVertex(end, color);

    this->addLineIndices(index, index+1);
}

void GraphicsManager::drawCircle(const Vector3& center, float radius, int segment_count, const Color& color)
{
    if (segment_count > 0)
    {
        int index = m_lineIndex;

        float increment = TwoPi / segment_count;
        float angle = 0.0f;

        for (int i = 0; i < segment_count; i++)
        {
            Vector3 vertex(center.x + radius * cos(angle), center.y, center.z + radius * sin(angle));

            this->addLineVertex(vertex, color);

            angle += increment;
        }

        for (int i = 0; i < segment_count; i++)
        {
            this->addLineIndices(index + i, i + 1 < segment_count ? index + i + 1 : index);
        }
    }
}

void GraphicsManager::drawQuad(const Vector3& center, float width, float height, const Color& color)
{
    int index = m_lineIndex;

    float x = width * 0.5;
    float y = height * 0.5;

    this->addLineVertex(center + Vector3(-x, 0, -y), color);
    this->addLineVertex(center + Vector3( x, 0, -y), color);
    this->addLineVertex(center + Vector3( x, 0,  y), color);
    this->addLineVertex(center + Vector3(-x, 0,  y), color);

    this->addLineIndices(index    , index + 1);
    this->addLineIndices(index + 1, index + 2);
    this->addLineIndices(index + 2, index + 3);
    this->addLineIndices(index + 3, index    );
}

void GraphicsManager::drawGrid(const Vector3& center, const Color& color, float cell_size, int numcols, int numrows)
{
    float width = numcols * cell_size;
    float height = numrows * cell_size;

    for (int i = 0; i < numrows; ++i)
    {
        Vector3 s = center +  Vector3(-width/2, 0, cell_size * i - height/2);
        Vector3 e = center +  Vector3(+width/2, 0, cell_size * i - height/2);

        this->drawLine(s, e, color);
    }

    for (int i = 0; i < numcols; ++i)
    {
        Vector3 s = center +  Vector3(cell_size * i - width/2, 0, -height/2);
        Vector3 e = center +  Vector3(cell_size * i - width/2, 0, +height/2);

        this->drawLine(s, e, color);
    }

    this->drawQuad(center, width, height, color);
}

void GraphicsManager::drawBox(const Vector3& center, const Vector3& size, const Color& color)
{
    if (size.y > 0)
    {
        int index = m_lineIndex;

        float y = size.y * 0.5;

        this->drawQuad(Vector3(center.x, center.y-y, center.z), size.x, size.z, color);
        this->drawQuad(Vector3(center.x, center.y+y, center.z), size.x, size.z, color);

        this->addLineIndices(index    , index + 4);
        this->addLineIndices(index + 1, index + 5);
        this->addLineIndices(index + 2, index + 6);
        this->addLineIndices(index + 3, index + 7);
    }
    else
    {
        this->drawQuad(center, size.x, size.z, color);
    }
}

void GraphicsManager::drawCone(const Vector3& center, float radius, float length, int segment_count, const Color& color)
{
    int index = m_lineIndex;

    this->drawCircle(center + Vector3(0, -length/2, 0), radius, segment_count, color);
    this->addLineVertex(center + Vector3(0, +length/2, 0), color);

    for (int i=0; i<segment_count; i++)
        this->addLineIndices(index + i, index + segment_count);
}

void GraphicsManager::drawCylinder(const Vector3& center, float radius, float length, int segment_count, const Color& color)
{
    int index = m_lineIndex;

    this->drawCircle(center + Vector3(0, -length*0.5, 0), radius, segment_count, color);
    this->drawCircle(center + Vector3(0, +length*0.5, 0), radius, segment_count, color);

    for (int i=0; i<segment_count; i++)
        this->addLineIndices(index + i, index + segment_count + i);
}

void GraphicsManager::renderPrimitives(RenderContext* rc)
{
    if (rc && m_lineVertices.size() > 0)
    {
        CameraNode* camera = rc->activeCamera();

        if (!camera) return;

        const float* t = 0;
        h3dGetNodeTransMats(camera->nodeHandle(), 0, &t);

        if (!t) return;

        Matrix4 trans_mat;
        memcpy(trans_mat.components, t, 16*sizeof(float));

        Matrix4 proj_mat;
        h3dGetCameraProjMat(camera->nodeHandle(), proj_mat.components);

        // Save OpenGL states.
        glPushAttrib(GL_ALL_ATTRIB_BITS);

        glMatrixMode(GL_PROJECTION);
        glLoadMatrixf(proj_mat.components);

        glMatrixMode(GL_MODELVIEW);
        glLoadMatrixf(trans_mat.invert().components);

        glLineWidth(1.0);
        glBegin(GL_LINES);

        for (int i=0; i<m_lineIndices.size(); i++)
        {
            int index = m_lineIndices.at(i);
            VertexDef v = m_lineVertices.at(index);

            glColor4f(v.color.r, v.color.g, v.color.b, v.color.a);
            glVertex3f(v.pos.x, v.pos.y, v.pos.z);
        }

        glEnd();

        glPopAttrib();
    }
}

void GraphicsManager::finalizeRender()
{
    h3dFinalizeFrame();

    m_lineVertices.clear();
    m_lineIndices.clear();
    m_lineIndex = 0;
}

void GraphicsManager::addLineVertex(const Vector3& vertex, const Color& color)
{
    m_lineVertices.push_back(VertexDef(vertex, color));
    ++m_lineIndex;
}

void GraphicsManager::addLineIndices(int index1, int index2)
{
    m_lineIndices.push_back(index1);
    m_lineIndices.push_back(index2);
}
